home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / pyshared / urlgrabber / byterange.py < prev    next >
Encoding:
Python Source  |  2009-09-25  |  16.7 KB  |  464 lines

  1. #   This library is free software; you can redistribute it and/or
  2. #   modify it under the terms of the GNU Lesser General Public
  3. #   License as published by the Free Software Foundation; either
  4. #   version 2.1 of the License, or (at your option) any later version.
  5. #
  6. #   This library is distributed in the hope that it will be useful,
  7. #   but WITHOUT ANY WARRANTY; without even the implied warranty of
  8. #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  9. #   Lesser General Public License for more details.
  10. #
  11. #   You should have received a copy of the GNU Lesser General Public
  12. #   License along with this library; if not, write to the 
  13. #      Free Software Foundation, Inc., 
  14. #      59 Temple Place, Suite 330, 
  15. #      Boston, MA  02111-1307  USA
  16.  
  17. # This file is part of urlgrabber, a high-level cross-protocol url-grabber
  18. # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
  19.  
  20.  
  21. import os
  22. import stat
  23. import urllib
  24. import urllib2
  25. import rfc822
  26.  
  27. DEBUG = None
  28.  
  29. try:    
  30.     from cStringIO import StringIO
  31. except ImportError, msg: 
  32.     from StringIO import StringIO
  33.  
  34. class RangeError(IOError):
  35.     """Error raised when an unsatisfiable range is requested."""
  36.     pass
  37.     
  38. class HTTPRangeHandler(urllib2.BaseHandler):
  39.     """Handler that enables HTTP Range headers.
  40.     
  41.     This was extremely simple. The Range header is a HTTP feature to
  42.     begin with so all this class does is tell urllib2 that the 
  43.     "206 Partial Content" reponse from the HTTP server is what we 
  44.     expected.
  45.     
  46.     Example:
  47.         import urllib2
  48.         import byterange
  49.         
  50.         range_handler = range.HTTPRangeHandler()
  51.         opener = urllib2.build_opener(range_handler)
  52.         
  53.         # install it
  54.         urllib2.install_opener(opener)
  55.         
  56.         # create Request and set Range header
  57.         req = urllib2.Request('http://www.python.org/')
  58.         req.header['Range'] = 'bytes=30-50'
  59.         f = urllib2.urlopen(req)
  60.     """
  61.     
  62.     def http_error_206(self, req, fp, code, msg, hdrs):
  63.         # 206 Partial Content Response
  64.         r = urllib.addinfourl(fp, hdrs, req.get_full_url())
  65.         r.code = code
  66.         r.msg = msg
  67.         return r
  68.     
  69.     def http_error_416(self, req, fp, code, msg, hdrs):
  70.         # HTTP's Range Not Satisfiable error
  71.         raise RangeError('Requested Range Not Satisfiable')
  72.  
  73. class HTTPSRangeHandler(HTTPRangeHandler):
  74.     """ Range Header support for HTTPS. """
  75.  
  76.     def https_error_206(self, req, fp, code, msg, hdrs):
  77.         return self.http_error_206(req, fp, code, msg, hdrs)
  78.  
  79.     def https_error_416(self, req, fp, code, msg, hdrs):
  80.         self.https_error_416(req, fp, code, msg, hdrs)
  81.  
  82. class RangeableFileObject:
  83.     """File object wrapper to enable raw range handling.
  84.     This was implemented primarilary for handling range 
  85.     specifications for file:// urls. This object effectively makes 
  86.     a file object look like it consists only of a range of bytes in 
  87.     the stream.
  88.     
  89.     Examples:
  90.         # expose 10 bytes, starting at byte position 20, from 
  91.         # /etc/aliases.
  92.         >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
  93.         # seek seeks within the range (to position 23 in this case)
  94.         >>> fo.seek(3)
  95.         # tell tells where your at _within the range_ (position 3 in
  96.         # this case)
  97.         >>> fo.tell()
  98.         # read EOFs if an attempt is made to read past the last
  99.         # byte in the range. the following will return only 7 bytes.
  100.         >>> fo.read(30)
  101.     """
  102.     
  103.     def __init__(self, fo, rangetup):
  104.         """Create a RangeableFileObject.
  105.         fo       -- a file like object. only the read() method need be 
  106.                     supported but supporting an optimized seek() is 
  107.                     preferable.
  108.         rangetup -- a (firstbyte,lastbyte) tuple specifying the range
  109.                     to work over.
  110.         The file object provided is assumed to be at byte offset 0.
  111.         """
  112.         self.fo = fo
  113.         (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
  114.         self.realpos = 0
  115.         self._do_seek(self.firstbyte)
  116.         
  117.     def __getattr__(self, name):
  118.         """This effectively allows us to wrap at the instance level.
  119.         Any attribute not found in _this_ object will be searched for
  120.         in self.fo.  This includes methods."""
  121.         if hasattr(self.fo, name):
  122.             return getattr(self.fo, name)
  123.         raise AttributeError, name
  124.     
  125.     def tell(self):
  126.         """Return the position within the range.
  127.         This is different from fo.seek in that position 0 is the 
  128.         first byte position of the range tuple. For example, if
  129.         this object was created with a range tuple of (500,899),
  130.         tell() will return 0 when at byte position 500 of the file.
  131.         """
  132.         return (self.realpos - self.firstbyte)
  133.     
  134.     def seek(self,offset,whence=0):
  135.         """Seek within the byte range.
  136.         Positioning is identical to that described under tell().
  137.         """
  138.         assert whence in (0, 1, 2)
  139.         if whence == 0:   # absolute seek
  140.             realoffset = self.firstbyte + offset
  141.         elif whence == 1: # relative seek
  142.             realoffset = self.realpos + offset
  143.         elif whence == 2: # absolute from end of file
  144.             # XXX: are we raising the right Error here?
  145.             raise IOError('seek from end of file not supported.')
  146.         
  147.         # do not allow seek past lastbyte in range
  148.         if self.lastbyte and (realoffset >= self.lastbyte):
  149.             realoffset = self.lastbyte
  150.         
  151.         self._do_seek(realoffset - self.realpos)
  152.         
  153.     def read(self, size=-1):
  154.         """Read within the range.
  155.         This method will limit the size read based on the range.
  156.         """
  157.         size = self._calc_read_size(size)
  158.         rslt = self.fo.read(size)
  159.         self.realpos += len(rslt)
  160.         return rslt
  161.     
  162.     def readline(self, size=-1):
  163.         """Read lines within the range.
  164.         This method will limit the size read based on the range.
  165.         """
  166.         size = self._calc_read_size(size)
  167.         rslt = self.fo.readline(size)
  168.         self.realpos += len(rslt)
  169.         return rslt
  170.     
  171.     def _calc_read_size(self, size):
  172.         """Handles calculating the amount of data to read based on
  173.         the range.
  174.         """
  175.         if self.lastbyte:
  176.             if size > -1:
  177.                 if ((self.realpos + size) >= self.lastbyte):
  178.                     size = (self.lastbyte - self.realpos)
  179.             else:
  180.                 size = (self.lastbyte - self.realpos)
  181.         return size
  182.         
  183.     def _do_seek(self,offset):
  184.         """Seek based on whether wrapped object supports seek().
  185.         offset is relative to the current position (self.realpos).
  186.         """
  187.         assert offset >= 0
  188.         if not hasattr(self.fo, 'seek'):
  189.             self._poor_mans_seek(offset)
  190.         else:
  191.             self.fo.seek(self.realpos + offset)
  192.         self.realpos+= offset
  193.         
  194.     def _poor_mans_seek(self,offset):
  195.         """Seek by calling the wrapped file objects read() method.
  196.         This is used for file like objects that do not have native
  197.         seek support. The wrapped objects read() method is called
  198.         to manually seek to the desired position.
  199.         offset -- read this number of bytes from the wrapped
  200.                   file object.
  201.         raise RangeError if we encounter EOF before reaching the 
  202.         specified offset.
  203.         """
  204.         pos = 0
  205.         bufsize = 1024
  206.         while pos < offset:
  207.             if (pos + bufsize) > offset:
  208.                 bufsize = offset - pos
  209.             buf = self.fo.read(bufsize)
  210.             if len(buf) != bufsize:
  211.                 raise RangeError('Requested Range Not Satisfiable')
  212.             pos+= bufsize
  213.  
  214. class FileRangeHandler(urllib2.FileHandler):
  215.     """FileHandler subclass that adds Range support.
  216.     This class handles Range headers exactly like an HTTP
  217.     server would.
  218.     """
  219.     def open_local_file(self, req):
  220.         import mimetypes
  221.         import mimetools
  222.         host = req.get_host()
  223.         file = req.get_selector()
  224.         localfile = urllib.url2pathname(file)
  225.         stats = os.stat(localfile)
  226.         size = stats[stat.ST_SIZE]
  227.         modified = rfc822.formatdate(stats[stat.ST_MTIME])
  228.         mtype = mimetypes.guess_type(file)[0]
  229.         if host:
  230.             host, port = urllib.splitport(host)
  231.             if port or socket.gethostbyname(host) not in self.get_names():
  232.                 raise urllib2.URLError('file not on local host')
  233.         fo = open(localfile,'rb')
  234.         brange = req.headers.get('Range',None)
  235.         brange = range_header_to_tuple(brange)
  236.         assert brange != ()
  237.         if brange:
  238.             (fb,lb) = brange
  239.             if lb == '': lb = size
  240.             if fb < 0 or fb > size or lb > size:
  241.                 raise RangeError('Requested Range Not Satisfiable')
  242.             size = (lb - fb)
  243.             fo = RangeableFileObject(fo, (fb,lb))
  244.         headers = mimetools.Message(StringIO(
  245.             'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' %
  246.             (mtype or 'text/plain', size, modified)))
  247.         return urllib.addinfourl(fo, headers, 'file:'+file)
  248.  
  249.  
  250. # FTP Range Support 
  251. # Unfortunately, a large amount of base FTP code had to be copied
  252. # from urllib and urllib2 in order to insert the FTP REST command.
  253. # Code modifications for range support have been commented as 
  254. # follows:
  255. # -- range support modifications start/end here
  256.  
  257. from urllib import splitport, splituser, splitpasswd, splitattr, \
  258.                    unquote, addclosehook, addinfourl
  259. import ftplib
  260. import socket
  261. import sys
  262. import mimetypes
  263. import mimetools
  264.  
  265. class FTPRangeHandler(urllib2.FTPHandler):
  266.     def ftp_open(self, req):
  267.         host = req.get_host()
  268.         if not host:
  269.             raise IOError, ('ftp error', 'no host given')
  270.         host, port = splitport(host)
  271.         if port is None:
  272.             port = ftplib.FTP_PORT
  273.         else:
  274.             port = int(port)
  275.  
  276.         # username/password handling
  277.         user, host = splituser(host)
  278.         if user:
  279.             user, passwd = splitpasswd(user)
  280.         else:
  281.             passwd = None
  282.         host = unquote(host)
  283.         user = unquote(user or '')
  284.         passwd = unquote(passwd or '')
  285.         
  286.         try:
  287.             host = socket.gethostbyname(host)
  288.         except socket.error, msg:
  289.             raise urllib2.URLError(msg)
  290.         path, attrs = splitattr(req.get_selector())
  291.         dirs = path.split('/')
  292.         dirs = map(unquote, dirs)
  293.         dirs, file = dirs[:-1], dirs[-1]
  294.         if dirs and not dirs[0]:
  295.             dirs = dirs[1:]
  296.         try:
  297.             fw = self.connect_ftp(user, passwd, host, port, dirs)
  298.             type = file and 'I' or 'D'
  299.             for attr in attrs:
  300.                 attr, value = splitattr(attr)
  301.                 if attr.lower() == 'type' and \
  302.                    value in ('a', 'A', 'i', 'I', 'd', 'D'):
  303.                     type = value.upper()
  304.             
  305.             # -- range support modifications start here
  306.             rest = None
  307.             range_tup = range_header_to_tuple(req.headers.get('Range',None))    
  308.             assert range_tup != ()
  309.             if range_tup:
  310.                 (fb,lb) = range_tup
  311.                 if fb > 0: rest = fb
  312.             # -- range support modifications end here
  313.             
  314.             fp, retrlen = fw.retrfile(file, type, rest)
  315.             
  316.             # -- range support modifications start here
  317.             if range_tup:
  318.                 (fb,lb) = range_tup
  319.                 if lb == '': 
  320.                     if retrlen is None or retrlen == 0:
  321.                         raise RangeError('Requested Range Not Satisfiable due to unobtainable file length.')
  322.                     lb = retrlen
  323.                     retrlen = lb - fb
  324.                     if retrlen < 0:
  325.                         # beginning of range is larger than file
  326.                         raise RangeError('Requested Range Not Satisfiable')
  327.                 else:
  328.                     retrlen = lb - fb
  329.                     fp = RangeableFileObject(fp, (0,retrlen))
  330.             # -- range support modifications end here
  331.             
  332.             headers = ""
  333.             mtype = mimetypes.guess_type(req.get_full_url())[0]
  334.             if mtype:
  335.                 headers += "Content-Type: %s\n" % mtype
  336.             if retrlen is not None and retrlen >= 0:
  337.                 headers += "Content-Length: %d\n" % retrlen
  338.             sf = StringIO(headers)
  339.             headers = mimetools.Message(sf)
  340.             return addinfourl(fp, headers, req.get_full_url())
  341.         except ftplib.all_errors, msg:
  342.             raise IOError, ('ftp error', msg), sys.exc_info()[2]
  343.  
  344.     def connect_ftp(self, user, passwd, host, port, dirs):
  345.         fw = ftpwrapper(user, passwd, host, port, dirs)
  346.         return fw
  347.  
  348. class ftpwrapper(urllib.ftpwrapper):
  349.     # range support note:
  350.     # this ftpwrapper code is copied directly from
  351.     # urllib. The only enhancement is to add the rest
  352.     # argument and pass it on to ftp.ntransfercmd
  353.     def retrfile(self, file, type, rest=None):
  354.         self.endtransfer()
  355.         if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1
  356.         else: cmd = 'TYPE ' + type; isdir = 0
  357.         try:
  358.             self.ftp.voidcmd(cmd)
  359.         except ftplib.all_errors:
  360.             self.init()
  361.             self.ftp.voidcmd(cmd)
  362.         conn = None
  363.         if file and not isdir:
  364.             # Use nlst to see if the file exists at all
  365.             try:
  366.                 self.ftp.nlst(file)
  367.             except ftplib.error_perm, reason:
  368.                 raise IOError, ('ftp error', reason), sys.exc_info()[2]
  369.             # Restore the transfer mode!
  370.             self.ftp.voidcmd(cmd)
  371.             # Try to retrieve as a file
  372.             try:
  373.                 cmd = 'RETR ' + file
  374.                 conn = self.ftp.ntransfercmd(cmd, rest)
  375.             except ftplib.error_perm, reason:
  376.                 if str(reason)[:3] == '501':
  377.                     # workaround for REST not supported error
  378.                     fp, retrlen = self.retrfile(file, type)
  379.                     fp = RangeableFileObject(fp, (rest,''))
  380.                     return (fp, retrlen)
  381.                 elif str(reason)[:3] != '550':
  382.                     raise IOError, ('ftp error', reason), sys.exc_info()[2]
  383.         if not conn:
  384.             # Set transfer mode to ASCII!
  385.             self.ftp.voidcmd('TYPE A')
  386.             # Try a directory listing
  387.             if file: cmd = 'LIST ' + file
  388.             else: cmd = 'LIST'
  389.             conn = self.ftp.ntransfercmd(cmd)
  390.         self.busy = 1
  391.         # Pass back both a suitably decorated object and a retrieval length
  392.         return (addclosehook(conn[0].makefile('rb'),
  393.                             self.endtransfer), conn[1])
  394.  
  395.  
  396. ####################################################################
  397. # Range Tuple Functions
  398. # XXX: These range tuple functions might go better in a class.
  399.  
  400. _rangere = None
  401. def range_header_to_tuple(range_header):
  402.     """Get a (firstbyte,lastbyte) tuple from a Range header value.
  403.     
  404.     Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
  405.     function pulls the firstbyte and lastbyte values and returns
  406.     a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
  407.     the header value, it is returned as an empty string in the
  408.     tuple.
  409.     
  410.     Return None if range_header is None
  411.     Return () if range_header does not conform to the range spec 
  412.     pattern.
  413.     
  414.     """
  415.     global _rangere
  416.     if range_header is None: return None
  417.     if _rangere is None:
  418.         import re
  419.         _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
  420.     match = _rangere.match(range_header)
  421.     if match: 
  422.         tup = range_tuple_normalize(match.group(1,2))
  423.         if tup and tup[1]: 
  424.             tup = (tup[0],tup[1]+1)
  425.         return tup
  426.     return ()
  427.  
  428. def range_tuple_to_header(range_tup):
  429.     """Convert a range tuple to a Range header value.
  430.     Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
  431.     if no range is needed.
  432.     """
  433.     if range_tup is None: return None
  434.     range_tup = range_tuple_normalize(range_tup)
  435.     if range_tup:
  436.         if range_tup[1]: 
  437.             range_tup = (range_tup[0],range_tup[1] - 1)
  438.         return 'bytes=%s-%s' % range_tup
  439.     
  440. def range_tuple_normalize(range_tup):
  441.     """Normalize a (first_byte,last_byte) range tuple.
  442.     Return a tuple whose first element is guaranteed to be an int
  443.     and whose second element will be '' (meaning: the last byte) or 
  444.     an int. Finally, return None if the normalized tuple == (0,'')
  445.     as that is equivelant to retrieving the entire file.
  446.     """
  447.     if range_tup is None: return None
  448.     # handle first byte
  449.     fb = range_tup[0]
  450.     if fb in (None,''): fb = 0
  451.     else: fb = int(fb)
  452.     # handle last byte
  453.     try: lb = range_tup[1]
  454.     except IndexError: lb = ''
  455.     else:  
  456.         if lb is None: lb = ''
  457.         elif lb != '': lb = int(lb)
  458.     # check if range is over the entire file
  459.     if (fb,lb) == (0,''): return None
  460.     # check that the range is valid
  461.     if lb < fb: raise RangeError('Invalid byte range: %s-%s' % (fb,lb))
  462.     return (fb,lb)
  463.  
  464.